import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  forwardRef,
  Inject,
  Injectable,
  InternalServerErrorException,
  NotFoundException,
  OnModuleInit,
} from '@nestjs/common'
import MeiliSearch from 'meilisearch'
import { ConfigService } from '@nestjs/config'
import { Repository } from 'typeorm'
import { InjectRepository } from '@nestjs/typeorm'
import {
  CreateReviewDto,
  makeMeiliComment,
  MeiliIndexedComment,
  Report,
  Resource,
  ResourceComment,
  ResourceReportStatus,
} from '@boticord/resources-client'
import { commentsMeiliSettings, ErrorCodes } from '@boticord/consts'
import { Action, BoticordAbility, JwtPayloadDto } from '@boticord/auth'
import { User } from '@boticord/users-client'
import { ResourcesService } from './resources.service'

@Injectable()
export class CommentsService implements OnModuleInit {
  private readonly meili: MeiliSearch

  constructor(
    @InjectRepository(ResourceComment) private readonly comments: Repository<ResourceComment>,
    @InjectRepository(Resource) private readonly resources: Repository<Resource>,
    @InjectRepository(User) private readonly users: Repository<User>,
    @InjectRepository(Report) private readonly reports: Repository<Report>,
    @Inject(forwardRef(() => ResourcesService)) private readonly resourcesService: ResourcesService,
    private readonly config: ConfigService,
  ) {
    this.meili = new MeiliSearch({
      host: config.getOrThrow('MEILISEARCH_HOST'),
      apiKey: config.getOrThrow('MEILI_MASTER_KEY'),
    })
  }

  async onModuleInit() {
    await this.ensureSettings()
  }

  private get index() {
    return this.meili.index<MeiliIndexedComment>('comments')
  }

  private async ensureSettings() {
    await this.index.updateSettings(commentsMeiliSettings)
  }

  async indexate(id: string) {
    const comment = await this.comments.findOne({ where: { id }, relations: { resource: true, author: true } })

    if (comment === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownComment })
    }

    await this.index.addDocuments([ makeMeiliComment(comment) ])
  }

  async readAll(id: string) {
    const resource = await this.resources.findOne({ where: { id } })
    if (resource === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownResource })
    }

    return await this.comments.find({ where: { resource: { id: resource.id } }, relations: { author: true } })
  }

  async create(id: string, body: CreateReviewDto, ability: BoticordAbility, payload: JwtPayloadDto) {
    if (!id) throw new NotFoundException({ code: ErrorCodes.UnknownResource })

    const resource = await this.resources.findOne({ where: { id } })

    if (resource === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownResource })
    }

    const user = await this.users.findOne({ where: { id: payload.id } })

    if (user === null) {
      throw new ForbiddenException({ code: ErrorCodes.UnknownUser })
    }

    const alreadyCommented = await this.comments.findOne({
      where: { author: { id: user.id }, resource: { id: resource.id } },
    })

    if (alreadyCommented) {
      throw new ConflictException({ code: ErrorCodes.AlreadyCommented })
    }

    const comment = new ResourceComment()

    comment.rating = body.rating
    comment.content = body.content
    comment.resource = resource
    comment.author = user

    const can = ability.can(Action.Create, comment)

    if (!can) {
      throw new ForbiddenException({ code: ErrorCodes.PermissionDenied })
    }

    const index = resource.ratings.findIndex(r => r.rating === body.rating)
    if (index === -1) throw new BadRequestException({ code: ErrorCodes.InvalidRating })
    resource.ratings[index].count++

    await this.comments.save(comment)
    await this.resources.save(resource)
    await this.resourcesService.indexate(resource.id)
    await this.indexate(comment.id)

    return comment
  }

  async update(id: string, body: CreateReviewDto, ability: BoticordAbility, payload: JwtPayloadDto) {
    const comment = await this.comments.findOne({ where: { id }, relations: [ 'resource' ] })

    if (comment === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownComment })
    }

    const user = await this.users.findOne({ where: { id: payload.id } })

    if (user === null) {
      throw new ForbiddenException({ code: ErrorCodes.UnknownUser })
    }

    if (comment.rating !== body.rating) {
      const oldIndex = comment.resource.ratings.findIndex(r => r.rating === comment.rating)
      if (oldIndex === -1) throw new BadRequestException({ code: ErrorCodes.InvalidRating })
      comment.resource.ratings[oldIndex].count--

      const index = comment.resource.ratings.findIndex(r => r.rating === body.rating)
      if (index === -1) throw new BadRequestException({ code: ErrorCodes.InvalidRating })
      comment.resource.ratings[index].count++

      await this.resources.save(comment.resource)
    }

    comment.rating = body.rating
    comment.content = body.content

    await this.comments.save(comment)
    await this.resourcesService.indexate(comment.resource.id)
    await this.indexate(comment.id)

    return comment
  }

  async delete(id: string, resourceID) {
    const comment = await this.comments.findOne({ where: { id }, relations: { resource: true } })
    if (comment === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownComment })
    }

    const notSolvedReport = await this.reports.findOne({
      where: { resource: { id: comment.id }, status: ResourceReportStatus.Pending },
    })

    if (notSolvedReport) {
      throw new ForbiddenException({ code: ErrorCodes.ReviewReported })
    }

    const index = comment.resource.ratings.findIndex(r => r.rating === comment.rating)
    if (index === -1) throw new InternalServerErrorException({ code: ErrorCodes.ReviewRatingsConflict })
    comment.resource.ratings[index].count--
    await this.resources.save(comment.resource)

    await this.reports.delete({ resource: { id: comment.id } })
    await this.comments.remove([ comment ]).then(e => e.length > 0 ? e[0] : undefined)

    await this.resourcesService.indexate(resourceID)
    await this.index.deleteDocument(id)

    return comment
  }

  async reply(id: string, reply: string) {
    const comment = await this.comments.findOne({ where: { id } })
    if (comment === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownComment })
    }

    comment.modReply = reply

    await this.comments.save(comment)
    await this.indexate(comment.id)

    return reply
  }

  async editReply(id: string, reply: string) {
    return this.reply(id, reply)
  }

  async deleteReply(id: string) {
    const comment = await this.comments.findOne({ where: { id } })
    if (comment === null) {
      throw new NotFoundException({ code: ErrorCodes.UnknownComment })
    }

    // @ts-ignore
    comment.modReply = null

    await this.comments.save(comment)
    await this.indexate(comment.id)

    return comment
  }

}
